package com.gitee.huanminabc.utils_tools.redis.redisTemplate.utils;

import com.gitee.huanminabc.jcommon.str.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.Collections;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;

//非红锁机制,只是简单的分布式锁,这种在主节点宕机还没来得及同步到从节点的时候,会出现锁丢失问题, 如果不是在对数据的一致性要求很高的情况下,可以使用
//如果数据要求一致性很高,可以使用红锁机制RedissonLockUtil
@Component
@Slf4j
public class RedisDistributeLock {

    //加锁 , 先判断没有人持有锁,或者当前线程持有锁,则加锁
    private static final String lockLua = "" +
            "  if ( redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 )" +
            "   then" +
            "       redis.call('hincrby', KEYS[1], ARGV[1], 1);" +
            "       redis.call('expire', KEYS[1], ARGV[2]);" +
            "       return 1 " +
            "   else " +
            "        return 0 " +
            " end ";
    //解锁 , 先判断是否持有锁,如果持有锁,则解锁
    private static final String unlockLua = "" +
            "  if(redis.call('hexists', KEYS[1], ARGV[1]) == 0) then" +
            "     return nil " +
            "  elseif(redis.call('hincrby', KEYS[1], ARGV[1], -1) > 0) then" +
            "     return 0  " +
            "  else " +
            "     redis.call('del', KEYS[1]) " +
            "     return 1 " +
            "end ";
    //续期 , 先判断是否持有锁,如果持有锁,则续期
    private static final String renewExpireLua = "" +
            " if( redis.call('hexists', KEYS[1], ARGV[1]) == 1 ) " +
            " then " +
            " redis.call('expire', KEYS[1], ARGV[2]); " +
            " return 1  " +
            " else " +
            " return 0 " +
            "end";


    @Autowired
    private RedisTemplateUtil redisTemplateUtil;
    //定时器
    private static final Timer TIMER = new Timer();
    //用于重入锁
    private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    private static final int RETRY_Limit = 3; //重试次数,一般来说3次就够了,多了也没用

    //只支持锁和重入锁,不支持续期和重试
    public void execute(String lockName, long expire, Runnable runnable) {
        execute(lockName, expire, false, false,true, runnable);
    }

    //不解锁, 一般用于想占着资源在超时时间内不释放锁
    public void executeNoUnlock(String lockName, long expire, Runnable runnable) {
        execute(lockName, expire, false, false,false, runnable);
    }

    //支持续期,不支持重试
    public void executeRenewal(String lockName, long expire, Runnable runnable) {
        execute(lockName, expire, true, false,true, runnable);
    }

    //支持重试,不支持续期
    public void executeRetry(String lockName, long expire, Runnable runnable) {
        execute(lockName, expire, false, true,true, runnable);
    }

    //支持续期和重试
    public void executeAll(String lockName, long expire, Runnable runnable) {
        execute(lockName, expire, true, true,true, runnable);
    }


    //手动加锁
    public boolean lock(String lockName, Long expire, boolean retry) {
        String uuid = UUID.randomUUID().toString();
        THREAD_LOCAL.set(uuid);
        return lock(lockName, uuid, expire, retry);
    }

    //手动解锁, 因为手动解锁需要考虑到事物的问题, 必须在事物提交后解锁
    //这种方法一般都会在事物中使用, 非事物的场景可以使用上面的组合方法更加方便
    public void unlock(String lockName) {
        String uuid = THREAD_LOCAL.get();
        if (StringUtil.isEmpty(uuid)) {
            log.warn("没有找到锁,lockName: {},请先使用lock方法加锁", lockName);
            return;
        }
        //如果当前没有事物,则直接解锁
        if (!TransactionSynchronizationManager.isActualTransactionActive()) {
            unlock(lockName, uuid);
        }else{
            //如果当前存在事物,那么就必须在事物提交后解锁,否则会出现数据不一致的情况
            try {
                TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                    @Override
                    public void afterCommit() {
                        unlock(lockName, uuid);
                    }
                });
            } catch (Exception e) {
                // 任何异常都释放锁
                unlock(lockName, uuid);
            }finally {
                // 清除掉ThreadLocal中的数据，避免内存溢出
                THREAD_LOCAL.remove();
            }
        }

    }

    //手动续期
    public void renewExpire(String lockName, Long expire) {
        String uuid = THREAD_LOCAL.get();
        if (StringUtil.isEmpty(uuid)) {
            log.warn("没有找到锁,lockName: {},请先使用lock方法加锁", lockName);
            return;
        }
        renewExpire(lockName, uuid, expire);
    }

    //==================================================================================================================

    /**
     * 执行业务逻辑 支持重入锁,和续期,拿不到锁重试
     *
     * @param lockName 锁名
     * @param expire   锁的生命周期，单位：秒 到期后自动释放锁
     * @param renewal  是否需要续期
     * @param retry    是否需要重试
     * @param unlock   是否需要解锁
     * @param runnable 业务逻辑
     */
    private void execute(String lockName, Long expire, boolean renewal, boolean retry,boolean unlock, Runnable runnable) {
        //获取当前线程的uuid,用于重入锁
        String uuid = THREAD_LOCAL.get();
        if (StringUtil.isEmpty(uuid)) {
            uuid = UUID.randomUUID().toString();
            THREAD_LOCAL.set(uuid);
        }
        //加锁如果成功,则执行业务逻辑
        if (lock(lockName, uuid, expire, retry)) {
            //是否需要续期
            if (renewal) {
                renewExpire(lockName, uuid, expire);
            }
            //执行业务逻辑
            try {
                runnable.run();
            } finally {
                if (unlock){
                    //解锁
                    unlock(lockName, uuid);
                } else {
                    // 清除掉ThreadLocal中的数据，避免内存溢出
                    THREAD_LOCAL.remove();
                }
            }
        }else {//加锁失败
            THREAD_LOCAL.remove();
        }
    }





    //加锁(默认支持重入锁 ,前是锁的名称一致)
    private boolean lock(String lockName, String uuid, Long expire, boolean retry) {
        //加锁
        try {
            if (!this.redisTemplateUtil.executeLuaBool(lockLua, Collections.singletonList(lockName), uuid, expire.toString())) {
                //加锁失败,判断是否需要重试
                if (retry) {
                    for (int i = 0; i < RETRY_Limit; i++) {
                        try {
                            Thread.sleep(100*i); //重试时间,递增的方式,3次重试,第一次100ms,第二次200ms,第三次300ms
                        } catch (InterruptedException e) {
                            log.error("lock failed, lockName: {} with request: {}", lockName, uuid);
                            Thread.currentThread().interrupt();
                        }
                        //没有获取到锁重试
                        boolean lock = lock(lockName, uuid, expire, retry);
                        if (lock) {
                            return true;
                        }
                    }
                    log.warn("lock failed, lockName: {} with request: {}", lockName, uuid);
                    return false;
                } else {//如果不需要重试,直接返回失败
                    log.warn("lock failed, lockName: {} with request: {}", lockName, uuid);
                    return false;
                }
            }
        } catch (Exception e) {
            log.error("lock failed, lockName: {} with request: {} ", lockName, uuid, e);
            return false;
        }
        return true;
    }

    //解锁, 如果当前存在事物,那么就必须在事物提交后解锁,否则会出现数据不一致的情况
    private void unlock(String lockName, String uuid) {
        //如果返回值没有使用Boolean，Spring-data-redis 进行类型转换时将会把 null
        //转为 false，这就会影响我们逻辑判断
        //所以返回类型只好使用 Long：null-解锁失败；0-重入次数减1；1-解锁成功。
        try {
            Long result = this.redisTemplateUtil.executeLuaLong(unlockLua, Collections.singletonList(lockName), uuid);
            // 如果未返回值，代表锁不存在
            if (result == null) {
                log.warn("attempt to unlock lock, not locked by lockName: {} with request: {}", lockName, uuid);
                THREAD_LOCAL.remove();
            } else if (result == 1) {// 解锁成功
                THREAD_LOCAL.remove();
            }
            //如果是其他的情况,则不解锁,因为可能是重入锁
        } catch (Exception e) { //出现异常释放内存
            log.error("unlock failed, lockName: {} with request: {}", lockName, uuid, e);
            THREAD_LOCAL.remove();
        }
    }

    /**
     * 开启定时器，自动续期 , 用于加锁成功后，续期
     * 防止业务逻辑执行时间过长，导致锁过期
     */
    private void renewExpire(String lockName, String uuid, Long expire) {
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                //续期 ,先判断是否还持有锁,如果持有锁,则续期, 如果锁被释放,则不续期
                Boolean b = redisTemplateUtil.executeLuaBool(renewExpireLua, Collections.singletonList(lockName), uuid, expire.toString());
                if (b) {//续期成功就继续续期
                    renewExpire(lockName, uuid, expire);
                } else {
                    log.warn("renewExpire failed, lockName: {} with request: {}", lockName, uuid);
                }
            }
        };
        TIMER.schedule(timerTask, expire * 1000 / 2 + expire * 1000 / 3); //  3/4的过期时间后开始续期
    }

}
